6.OpenGL ES绘制矩形及圆形
写完上面的绘制三角形的部分,手快抽筋了,为啥?
一个就是每个方法都要调用GLES30.还有一个就是我们完全可以把公共的代码再封装到一个Base类里面啊,所以我这里就抽了一个BaseGLSurfaceViewRenderer类,然后把GLES30中一些常用的方法写了一遍。
为什么要这样做呢? 我们也可以通过import static来省去GLES30的写法,但我不喜欢那样。另外还弄了BufferUtil、ProjectionMatrixUtil的类:
public class BufferUtil {
private static final int BYTES_PER_FLOAT = 4;
public static FloatBuffer getFloatBuffer(float[] array) {
FloatBuffer floatBuffer = ByteBuffer.allocateDirect(array.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
floatBuffer.put(array);
floatBuffer.position(0);
return floatBuffer;
}
}
public class ProjectionMatrixUtil {
// 矩阵数组
private static final float[] mProjectionMatrix = new float[]{
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
};
public static void orthoM(int program, int width, int height, String name) {
int uMatrixLocation = GLES30.glGetUniformLocation(program, name);
//计算宽高比 边长比(>=1),非宽高比
float aspectRatio = width > height ?
(float) width / (float) height :
(float) height / (float) width;
if (width > height) {
// 横屏
Matrix.orthoM(mProjectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f);
} else {
// 竖屏or正方形
Matrix.orthoM(mProjectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f);
}
GLES30.glUniformMatrix4fv(uMatrixLocation, 1, false, mProjectionMatrix, 0);
}
}
public abstract class BaseGLSurfaceViewRenderer implements GLSurfaceView.Renderer {
protected int mProgram;
/**
* readResource -> compileShader -> linkProgram -> useProgram
*
* @param context
* @param vertexShader
* @param fragmentShader
*/
protected void handleProgram(@NonNull Context context, @RawRes int vertexShader, @RawRes int fragmentShader) {
String vertexShaderStr = ResReadUtils.readResource(context, vertexShader);
int vertexShaderId = ShaderUtils.compileVertexShader(vertexShaderStr);
//编译片段着色程序
String fragmentShaderStr = ResReadUtils.readResource(context, fragmentShader);
int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShaderStr);
//连接程序
mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId);
//在OpenGLES环境中使用程序
GLES30.glUseProgram(mProgram);
}
protected void glViewport(int x, int y, int width, int height) {
GLES30.glViewport(x, y, width, height);
}
protected void glClearColor(float red, float green, float blue, float alpha) {
GLES30.glClearColor(red, green, blue, alpha);
}
protected void glClear(int mask) {
GLES30.glClear(mask);
}
protected static void glEnableVertexAttribArray(int index) {
GLES30.glEnableVertexAttribArray(index);
}
protected void glDisableVertexAttribArray(int index) {
GLES30.glDisableVertexAttribArray(index);
}
protected int glGetAttribLocation(String name) {
return GLES30.glGetAttribLocation(mProgram, name);
}
protected int glGetUniformLocation(String name) {
return GLES30.glGetUniformLocation(mProgram, name);
}
protected void glUniformMatrix4fv(int location, int count, boolean transpose, float[] value, int offset) {
GLES30.glUniformMatrix4fv(location, count, transpose, value, offset);
}
protected void glDrawArrays(int mode, int first, int count) {
GLES30.glDrawArrays(mode, first, count);
}
protected void glDrawElements(int mode, int count, int type, int offset) {
GLES30.glDrawElements(mode, count, type, offset);
}
protected void orthoM(String name, int width, int height) {
ProjectionMatrixUtil.orthoM(mProgram, width, height, name);
}
protected void glVertexAttribPointer(
int indx,
int size,
int type,
boolean normalized,
int stride,
java.nio.Buffer ptr) {
GLES30.glVertexAttribPointer(indx, size, type, normalized, stride, ptr);
}
}
前面绘制点、线、三角形的时候在用GLES30.glDrawArrays(GL_TRIANGLE_STRIP)方法时选择的mode是GL_TRIANGLE_STRIP,这个mode还有其他的类型,假设我现在想要绘制一个6边形呢,这里以A、B、C、D、E、F六个点来说一下mode的区别:
- GL_POINTS : 绘制独立的点。
- GL_LINES : 绘制每两个点的一条线。AB、CD、EF
- GL_LINE_LOOP : 按顺序将所有的点都连接起来,包括收尾相连。AB、BC、CD、DE、EF、FA
- GL_LINE_STRIP:按顺序将所有的点连接起来,不包括收尾相连。AB、BC、CD、DE、EF
- GL_TRIANGLES:每3个点构成一个三角形。 ABC、DEF
- GL_TRIANGLES_STRIP:相邻3个点构成一个三角形,不包括收尾两个点。ABC、BCD、CDE、DEF
- GL_TRIANGLE_FAN:第一个点和之后所有相邻的两个点构成一个三角形。ABC、ACD、ADE、AEF
OpenGL ES中仅允许采用三角形来搭建物体。其实这从构造能力上来说并没有区别,因为任何多边形都可以拆分为多个三角形。OpenGL ES中之所以仅支持三角形而不支持任意多边形是出于性能的考虑,就目前移动嵌入式设备的硬件性能情况来看,这是必然的选择了。
顶点法和索引法
顶点法
上一篇文章中写的绘制点、线、三角形都是使用GLES30.glDrawArrays()来绘制,它是顶点法。根据传入的顶点顺序进行绘制。顶点复用情况少,可读性低。
索引法
根据索引序列,在顶点序列中找到对应的顶点,并根据绘制的方式,组成相应的图元绘制,用的是GLES30.glDrawElements(),称为索引法。
相对于顶点法在复杂图形的绘制中无法避免大量顶点重复的情况,索引法可以减少很多重复顶点占用的空间,所以复杂的图形下推荐使用索引法。顶点复用情况多,可读性高。
之前说过OpenGL ES提供的的图元单位是三角形,想要绘制其他多边形,就要利用三角形来拼成。 矩形是两个三角形,而圆形则是由很多个三角形组成,个数越多,圆越圆。
元素缓冲对象(EBO)
在渲染顶点这一话题上我们还有最后一个需要讨论的东西——元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO)。
要解释元素缓冲对象的工作方式最好还是举个例子:假设我们不再绘制一个三角形而是绘制一个矩形。
我们可以绘制两个三角形来组成一个矩形(OpenGL主要处理三角形)。这会生成下面的顶点的集合:
float vertices[] = {
// 第一个三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二个三角形
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
可以看到,有几个顶点叠加了。
我们指定了右下角和左上角两次!
一个矩形只有4个而不是6个顶点,这样就产生50%的额外开销。
当我们有包括上千个三角形的模型之后这个问题会更糟糕,这会产生一大堆浪费。
更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。如果OpenGL提供这个功能就好了,对吧?
值得庆幸的是,元素缓冲区对象的工作方式正是如此。 EBO是一个缓冲区,就像一个顶点缓冲区对象一样,它存储 OpenGL 用来决定要绘制哪些顶点的索引。
这种所谓的索引绘制(Indexed Drawing)正是我们问题的解决方案。首先,我们先要定义(不重复的)顶点,和绘制出矩形所需的索引:
float vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
int indices[] = {
// 注意索引从0开始!
// 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
// 这样可以由下标代表顶点组合成矩形
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
你可以看到,当使用索引的时候,我们只定义了4个顶点,而不是6个。下一步我们需要创建元素缓冲对象:
//创建 ebo
GLES30.glGenBuffers(1,ebo[0])
//绑定 ebo 到上下文
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, ebo[0])
//设置数据
GLES30.glBufferData(GLES30.GL_ELEMENT_ARRAY_BUFFER,
indexData.capacity() * 4,
indexData,
GLES30.GL_STATIC_DRAW
)
与VBO类似,我们先绑定EBO然后用glBufferData把索引复制到缓冲里。
同样,和VBO类似,我们会把这些函数调用放在绑定和解绑函数调用之间,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。
注意:我们传递了GL_ELEMENT_ARRAY_BUFFER当作缓冲目标。最后一件要做的事是用glDrawElements来替换glDrawArrays函数,表示我们要从索引缓冲区渲染三角形。
使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制:
GLES30.glDrawElements(GLES30.GL_TRIANGLE_STRIP, 6,GLES30.GL_UNSIGNED_INT, 0)
- 第一个参数指定了我们绘制的模式,这个和glDrawArrays的一样。
- 第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。
- 第三个参数是索引的类型,这里是GL_UNSIGNED_INT。
- 最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们会在这里填写0。
glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取其索引。
这意味着我们每次想要使用索引渲染对象时都必须绑定相应的EBO,这又有点麻烦。
碰巧顶点数组对象也跟踪元素缓冲区对象绑定。在绑定VAO时,绑定的最后一个元素缓冲区对象存储为VAO的元素缓冲区对象。然后,绑定到VAO也会自动绑定该EBO。
注意: EBO的绑定一定要在VAO解绑之前绑定!
当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个EBO配置了。
OpenGL中会解绑VBO和VAO但是不能解绑EBO)
到这里我有点懵了,VAO有什么用?
简单说就是VBO用于存储顶点数据,而VAO用于封装顶点数组的状态,包括顶点属性指针和VBO的绑定状态。这两者的结合使用可以使OpenGL中管理和切换多个顶点数据配置变得更加方便和高效。
绘制矩形
顶点着色器与上一个三角形的一样
片段着色器与上一个三角形的一样
Render的实现如下
顶点法:
public class SquareRender extends BaseGLSurfaceViewRenderer { //顶点位置缓存 private final FloatBuffer vertexBuffer; //顶点颜色缓存 private final FloatBuffer colorBuffer; //位置 private int aPositionLocation; //颜色 private int aColorLocation; /** * 坐标占用的向量个数 */ private static final int POSITION_COMPONENT_COUNT = 2; private static final float[] POINT_DATA = { -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f, }; /** * 颜色占用的向量个数 */ private static final int COLOR_COMPONENT_COUNT = 4; private static final float[] COLOR_DATA = { // 一个顶点有3个向量数据:r、g、b、a 1f, 0.5f, 0.5f, 0f, 1f, 0f, 1f, 0f, 0f, 1f, 1f, 0f, 1f, 1f, 0f, 0f }; public SquareRender() { vertexBuffer = BufferUtil.getFloatBuffer(POINT_DATA); colorBuffer = BufferUtil.getFloatBuffer(COLOR_DATA); } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { glClearColor(1.0f, 1.0f, 1.0f, 1.0f); handleProgram(MyApplication.getInstance(), R.raw.iso_triangle_vertex_shader, R.raw.iso_triangle_fragment_shader); aPositionLocation = glGetAttribLocation("vPosition"); aColorLocation = glGetAttribLocation("aColor"); glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexBuffer); glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, colorBuffer); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { glViewport(0, 0, width, height); orthoM("u_Matrix", width, height); } @Override public void onDrawFrame(GL10 gl) { glClear(GLES30.GL_COLOR_BUFFER_BIT); glEnableVertexAttribArray(aPositionLocation); glEnableVertexAttribArray(aColorLocation); // 正方形、四个点(POINT_DATA.length / POSITION_COMPONENT_COUNT) glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); glDisableVertexAttribArray(aPositionLocation); glDisableVertexAttribArray(aColorLocation); } }
上面是一个正方形,那如果我画一个六边形,就需要多定义好几个顶点数据,来让其能组成对应的三角形拼接,下面是用索引法来画一个六边形,索引法相对于顶点法来说可以更高效,能节省很多顶点的数据:
public class HexagonRender extends BaseGLSurfaceViewRenderer { //顶点位置缓存 private final FloatBuffer vertexBuffer; //顶点颜色缓存 private final FloatBuffer colorBuffer; // 顶点索引缓存 private final ShortBuffer indexBuffer; //位置 private int aPositionLocation; //颜色 private int aColorLocation; /** * 坐标占用的向量个数 */ private static final int POSITION_COMPONENT_COUNT = 2; private static final float[] POINT_DATA = { -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0f, -1.0f, 0f, 1.0f }; /** * 数组绘制的索引:当前是绘制三角形,所以是3个元素构成一个绘制顺序 */ private static final short[] INDEX_DATA = { 0, 1, 2, 0, 2, 3, 0, 4, 1, 3, 2, 5 }; /** * 颜色占用的向量个数 */ private static final int COLOR_COMPONENT_COUNT = 4; private static final float[] COLOR_DATA = { // 一个顶点有3个向量数据:r、g、b、a 1f, 0.5f, 0.5f, 0f, 1f, 0f, 1f, 0f, 0f, 1f, 1f, 0f, 1f, 1f, 0f, 0f }; public HexagonRender() { vertexBuffer = BufferUtil.getFloatBuffer(POINT_DATA); colorBuffer = BufferUtil.getFloatBuffer(COLOR_DATA); indexBuffer = BufferUtil.getShortBuffer(INDEX_DATA); } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { glClearColor(1.0f, 1.0f, 1.0f, 1.0f); handleProgram(MyApplication.getInstance(), R.raw.iso_triangle_vertex_shader, R.raw.iso_triangle_fragment_shader); aPositionLocation = glGetAttribLocation("vPosition"); aColorLocation = glGetAttribLocation("aColor"); glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexBuffer); glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, colorBuffer); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { glViewport(0, 0, width, height); orthoM("u_Matrix", width, height); } @Override public void onDrawFrame(GL10 gl) { glClear(GLES30.GL_COLOR_BUFFER_BIT); glEnableVertexAttribArray(aPositionLocation); glEnableVertexAttribArray(aColorLocation); // 绘制相对复杂的图形时,若顶点有较多重复时,对比数据占用空间而言,glDrawElements会比glDrawArrays小很多,也会更高效 // 因为在有重复顶点的情况下,glDrawArrays方式需要的3个顶点位置是用Float型的,占3*4的Byte值; // 而glDrawElements需要3个Short型的,占3*2Byte值 // 1. 图形绘制方式; 2. 绘制的顶点数; 3. 索引的数据格式; 4. 索引的数据Buffer glDrawElements(GLES30.GL_TRIANGLES, INDEX_DATA.length, GLES30.GL_UNSIGNED_SHORT, indexBuffer); glDisableVertexAttribArray(aPositionLocation); glDisableVertexAttribArray(aColorLocation); } }
绘制圆形
其他也都和上面的一样,只有Render不同,如下:
public class CircleRender extends BaseGLSurfaceViewRenderer {
//顶点位置缓存
private final FloatBuffer vertexBuffer;
//顶点颜色缓存
private final FloatBuffer colorBuffer;
//位置
private int aPositionLocation;
//颜色
private int aColorLocation;
/**
* 坐标占用的向量个数
*/
private static final int POSITION_COMPONENT_COUNT = 3;
private float circlePosition[];
/**
* 颜色占用的向量个数
*/
private static final int COLOR_COMPONENT_COUNT = 4;
private float color[];
public CircleRender() {
createPositions(1, 60);
vertexBuffer = BufferUtil.getFloatBuffer(circlePosition);
colorBuffer = BufferUtil.getFloatBuffer(color);
}
private void createPositions(int radius, int n){
ArrayList<Float> data=new ArrayList<>();
data.add(0.0f); //设置圆心坐标
data.add(0.0f);
data.add(0.0f);
float angDegSpan=360f/n;
for(float i=0;i<360+angDegSpan;i+=angDegSpan){
data.add((float) (radius*Math.sin(i*Math.PI/180f)));
data.add((float)(radius*Math.cos(i*Math.PI/180f)));
data.add(0.0f);
}
float[] f=new float[data.size()];
for (int i=0;i<f.length;i++){
f[i]=data.get(i);
}
circlePosition = f;
//处理各个顶点的颜色
color = new float[f.length*4/3];
ArrayList<Float> tempC = new ArrayList<>();
ArrayList<Float> totalC = new ArrayList<>();
tempC.add(1.0f);
tempC.add(0.0f);
tempC.add(0.0f);
tempC.add(1.0f);
for (int i=0;i<f.length/3;i++){
totalC.addAll(tempC);
}
for (int i=0; i<totalC.size();i++){
color[i]=totalC.get(i);
}
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
handleProgram(MyApplication.getInstance(), R.raw.iso_triangle_vertex_shader, R.raw.iso_triangle_fragment_shader);
aPositionLocation = glGetAttribLocation("vPosition");
aColorLocation = glGetAttribLocation("aColor");
glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexBuffer);
glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, colorBuffer);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
glViewport(0, 0, width, height);
orthoM("u_Matrix", width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
glClear(GLES30.GL_COLOR_BUFFER_BIT);
glEnableVertexAttribArray(aPositionLocation);
glEnableVertexAttribArray(aColorLocation);
glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, circlePosition.length / POSITION_COMPONENT_COUNT);
glDisableVertexAttribArray(aPositionLocation);
glDisableVertexAttribArray(aColorLocation);
}
}
OpenGL支持的图元
- 邮箱 :[email protected]
- Good Luck!